﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Hosting;

namespace Dexter.Storage.FileSystem {
	public class FileSystemStorageProvider : IStorageProvider {
		readonly string privatePath;
		readonly string publicPath;
		readonly string storagePath;

		/// <summary>
		/// 	Initializes a new instance of the <see cref = "T:System.Object" /> class.
		/// </summary>
		public FileSystemStorageProvider ( ) : this ( null , null ) {}

		public FileSystemStorageProvider ( string publicPath , string privatePath ) {
			storagePath = HostingEnvironment.IsHosted
							? HostingEnvironment.MapPath ( "~/" )
							: AppDomain.CurrentDomain.BaseDirectory;

			this.publicPath = string.IsNullOrEmpty ( publicPath )
			                  	? GetPath ( null )
			                  	: publicPath;

			this.privatePath = string.IsNullOrEmpty ( privatePath )
			                   	? GetPath ( "App_Data" )
			                   	: privatePath;
		}

		#region IStorageProvider Members

		/// <summary>
		/// 	Retrieves the public URL for a given file within the storage provider.
		/// </summary>
		/// <param name = "path">The relative path within the storage provider.</param>
		/// <returns>The public URL.</returns>
		public string GetPublicUrl ( string path ) {
			return MapPublic ( path );
		}

		/// <summary>
		/// 	Retrieves a file within the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the file within the storage provider.</param>
		/// <returns>The file.</returns>
		/// <exception cref = "ArgumentException">If the file is not found.</exception>
		public IStorageFile GetFile ( string path ) {
			FileInfo fileInfo = new FileInfo ( MapStorage ( path ) );
			if ( !fileInfo.Exists ) {
				throw new FileNotFoundException ( "File does not exist" , path );
			}

			return new FileSystemStorageFile ( Fix ( path ) , fileInfo );
		}

		/// <summary>
		/// 	Check if the specified file exists.
		/// </summary>
		/// <param name = "path">The path.</param>
		/// <returns><c>True</c> if the file exists into the repository. Otherwise <c>false</c>.</returns>
		public bool FileExist ( string path ) {
			FileInfo fileInfo = new FileInfo ( MapStorage ( path ) );

			return fileInfo.Exists;
		}

		/// <summary>
		/// 	Lists the files within a storage provider's path.
		/// </summary>
		/// <param name = "path">The relative path to the folder which files to list.</param>
		/// <returns>The list of files in the folder.</returns>
		public IEnumerable <IStorageFile> ListFiles ( string path ) {
			DirectoryInfo directoryInfo = new DirectoryInfo ( MapStorage ( path ) );
			if ( !directoryInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "Directory {0} does not exist" , path ) );
			}

			return directoryInfo
				.GetFiles ( )
				.Where ( fi => !IsHidden ( fi ) )
				.Select <FileInfo , IStorageFile> ( fi => new FileSystemStorageFile ( Path.Combine ( Fix ( path ) , fi.Name ) , fi ) )
				.ToList ( );
		}

		/// <summary>
		/// 	Lists the folders within a storage provider's path.
		/// </summary>
		/// <param name = "path">The relative path to the folder which folders to list.</param>
		/// <returns>The list of folders in the folder.</returns>
		public IEnumerable <IStorageFolder> ListFolders ( string path ) {
			DirectoryInfo directoryInfo = new DirectoryInfo ( MapStorage ( path ) );
			if ( !directoryInfo.Exists ) {
				try {
					directoryInfo.Create ( );
				}
				catch ( Exception ex ) {
					throw new ArgumentException ( string.Format ( "The folder could not be created at path: {0}. {1}" , path , ex ) );
				}
			}

			return directoryInfo
				.GetDirectories ( )
				.Where ( di => !IsHidden ( di ) )
				.Select <DirectoryInfo , IStorageFolder> ( di => new FileSystemStorageFolder ( Path.Combine ( Fix ( path ) , di.Name ) , di ) )
				.ToList ( );
		}

		/// <summary>
		/// 	Gets the folder.
		/// </summary>
		/// <param name = "path">The path.</param>
		/// <returns></returns>
		public IStorageFolder GetFolder ( string path ) {
			DirectoryInfo directoryInfo = new DirectoryInfo ( MapStorage ( path ) );

			return new FileSystemStorageFolder ( Path.Combine ( Fix ( path ) , directoryInfo.Name ) , directoryInfo );
		}

		/// <summary>
		/// 	Folders the exists.
		/// </summary>
		/// <param name = "path">The path.</param>
		/// <returns></returns>
		public bool FolderExists ( string path ) {
			DirectoryInfo directoryInfo = new DirectoryInfo ( MapStorage ( path ) );

			return directoryInfo.Exists;
		}

		/// <summary>
		/// 	Tries to create a folder in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the folder to be created.</param>
		/// <returns>True if success; False otherwise.</returns>
		public bool TryCreateFolder ( string path ) {
			try {
				CreateFolder ( path );
			}
			catch {
				return false;
			}

			return true;
		}

		/// <summary>
		/// 	Creates a folder in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the folder to be created.</param>
		/// <exception cref = "ArgumentException">If the folder already exists.</exception>
		public void CreateFolder ( string path ) {
			DirectoryInfo directoryInfo = new DirectoryInfo ( MapStorage ( path ) );
			if ( directoryInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "Directory {0} already exists" , path ) );
			}

			Directory.CreateDirectory ( directoryInfo.FullName );
		}

		/// <summary>
		/// 	Deletes a folder in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the folder to be deleted.</param>
		/// <exception cref = "ArgumentException">If the folder doesn't exist.</exception>
		public void DeleteFolder ( string path ) {
			DirectoryInfo directoryInfo = new DirectoryInfo ( MapStorage ( path ) );
			if ( !directoryInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "Directory {0} does not exist" , path ) );
			}

			directoryInfo.Delete ( true );
		}

		/// <summary>
		/// 	Renames a folder in the storage provider.
		/// </summary>
		/// <param name = "oldPath">The relative path to the folder to be renamed.</param>
		/// <param name = "newPath">The relative path to the new folder.</param>
		public void RenameFolder ( string oldPath , string newPath ) {
			DirectoryInfo sourceDirectory = new DirectoryInfo ( MapStorage ( oldPath ) );
			if ( !sourceDirectory.Exists ) {
				throw new ArgumentException ( string.Format ( "Directory {0} does not exist" , oldPath ) );
			}

			DirectoryInfo targetDirectory = new DirectoryInfo ( MapStorage ( newPath ) );
			if ( targetDirectory.Exists ) {
				throw new ArgumentException ( string.Format ( "Directory {0} already exists" , newPath ) );
			}

			Directory.Move ( sourceDirectory.FullName , targetDirectory.FullName );
		}

		/// <summary>
		/// 	Deletes a file in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the file to be deleted.</param>
		/// <exception cref = "ArgumentException">If the file doesn't exist.</exception>
		public void DeleteFile ( string path ) {
			FileInfo fileInfo = new FileInfo ( MapStorage ( path ) );
			if ( !fileInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "File {0} does not exist" , path ) );
			}

			fileInfo.Delete ( );
		}

		/// <summary>
		/// 	Renames a file in the storage provider.
		/// </summary>
		/// <param name = "oldPath">The relative path to the file to be renamed.</param>
		/// <param name = "newPath">The relative path to the new file.</param>
		public void RenameFile ( string oldPath , string newPath ) {
			FileInfo sourceFileInfo = new FileInfo ( MapStorage ( oldPath ) );
			if ( !sourceFileInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "File {0} does not exist" , oldPath ) );
			}

			FileInfo targetFileInfo = new FileInfo ( MapStorage ( newPath ) );
			if ( targetFileInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "File {0} already exists" , newPath ) );
			}

			File.Move ( sourceFileInfo.FullName , targetFileInfo.FullName );
		}

		/// <summary>
		/// 	Creates a file in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the file to be created.</param>
		/// <exception cref = "ArgumentException">If the file already exists.</exception>
		/// <returns>The created file.</returns>
		public IStorageFile CreateFile ( string path ) {
			FileInfo fileInfo = new FileInfo ( MapStorage ( path ) );
			if ( fileInfo.Exists ) {
				throw new ArgumentException ( string.Format ( "File {0} already exists" , fileInfo.Name ) );
			}

			// ensure the directory exists
			var dirName = Path.GetDirectoryName ( fileInfo.FullName );

			if ( dirName == null ) {
				throw new ArgumentNullException ( );
			}

			if ( !Directory.Exists ( dirName ) ) {
				Directory.CreateDirectory ( dirName );
			}
			File.WriteAllBytes ( fileInfo.FullName , new byte[0] );

			return new FileSystemStorageFile ( Fix ( path ) , fileInfo );
		}

		/// <summary>
		/// 	Tries to save a stream in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the file to be created.</param>
		/// <param name = "inputStream">The stream to be saved.</param>
		/// <returns>True if success; False otherwise.</returns>
		public bool TrySaveStream ( string path , Stream inputStream ) {
			try {
				SaveStream ( path , inputStream );
			}
			catch {
				return false;
			}

			return true;
		}

		/// <summary>
		/// 	Saves a stream in the storage provider.
		/// </summary>
		/// <param name = "path">The relative path to the file to be created.</param>
		/// <param name = "inputStream">The stream to be saved.</param>
		/// <exception cref = "ArgumentException">If the stream can't be saved due to access permissions.</exception>
		public void SaveStream ( string path , Stream inputStream ) {
			// Create the file.
			// The CreateFile method will map the still relative path
			var file = CreateFile ( path );

			var outputStream = file.OpenWrite ( );
			var buffer = new byte[8192];
			for ( ;; ) {
				var length = inputStream.Read ( buffer , 0 , buffer.Length );
				if ( length <= 0 ) {
					break;
				}
				outputStream.Write ( buffer , 0 , length );
			}
			outputStream.Dispose ( );
		}

		/// <summary>
		/// 	Combines to paths.
		/// </summary>
		/// <param name = "path1">The parent path.</param>
		/// <param name = "path2">The child path.</param>
		/// <returns>The combined path.</returns>
		public string Combine ( string path1 , string path2 ) {
			return Path.Combine ( path1 , path2 );
		}

		public string PrivateRoot {
			get { return privatePath; }
		}

		public string PublicRoot {
			get { return publicPath; }
		}

		#endregion

		string GetPath ( string startingPath ) {
			var appPath = string.Empty;
			if ( HostingEnvironment.IsHosted ) {
				appPath = string.IsNullOrEmpty ( startingPath )
				          	? storagePath
				          	: Path.Combine ( storagePath, startingPath );
				
			}

			return appPath;
		}

		string MapStorage ( string path ) {
			string mappedPath = string.IsNullOrEmpty ( path )
									? storagePath
									: Path.Combine ( storagePath, path );

			//return PathValidation.ValidatePath ( storagePath , mappedPath );

			return mappedPath;
		}

		string MapPublic ( string path ) {
			return string.IsNullOrEmpty ( path )
			       	? publicPath
			       	: Path.Combine ( publicPath , path ).Replace ( Path.DirectorySeparatorChar , '/' );
		}

		static string Fix ( string path ) {
			return string.IsNullOrEmpty ( path )
			       	? ""
			       	: Path.DirectorySeparatorChar != '/'
			       	  	? path.Replace ( '/' , Path.DirectorySeparatorChar )
			       	  	: path;
		}

		static bool IsHidden ( FileSystemInfo di ) {
			return ( di.Attributes & FileAttributes.Hidden ) != 0;
		}
	}
}